(ns json-rpc
  (:use util.utils)
  (:use [clojure.contrib.server-socket :only [create-server]]
        [clojure.java.io :only [reader writer]]
        clojure.tools.logging)
  (:require clojure.data.json)
  (:import (java.net Socket)
           (java.net InetSocketAddress)))



(def ^:dynamic *json-rpc-port* 7324)
(def ^:dynamic *json-rpc-timeout* 3)

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;; json-rpc client
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

(defn json-rpc-connect
  ([host port]
    (let [socket (Socket.)]
      (.connect socket (InetSocketAddress. host port) *json-rpc-timeout*)
      {:socket socket
       :in (reader socket)
       :out (writer socket)}))
  ([host]
    (if (sequential? host)
      (json-rpc-connect (first host) (second host))
      (if-let [[_ host port] (re-matches #"(.+):(\d+)" host)]
        (json-rpc-connect host port)
        (json-rpc-connect host *json-rpc-port*)))))

(defn- json-rpc-write [out v]
  "Simple json object write to socket connection"
  (doto out
    (.println (clojure.data.json/json-str v))
    (.flush)))

(defn- json-rpc-read [in]
  "Simple json object read from socket connection"
  (when-let [line (.readLine in)]
    (clojure.data.json/read-json line false)))

(defn- json-rpc-with [connection method params id]
  (json-rpc-write (:out connection) {"method" method "params" params "id" id})
  (let [{result "result" error "error" id "id"} (json-rpc-read (:in connection))]
    (when-not (nil? error)
      (throw (Exception. error)))
    result))

(def ^:dynamic *json-rpc-connection* nil)

(defmacro with-json-rpc [host & body]
  `(binding [*json-rpc-connection* (json-rpc-connect ~host)]
    (with-open [s# (:socket *json-rpc-connection*)]
      (println s#)
      ~@body)))

(defn json-rpc
  ([method params id] ; XXX: probably I should replace [method params id] with [host method params]
    (when-not *json-rpc-connection*
      (throw (Exception. "No json-rpc connection")))
    (json-rpc-with *json-rpc-connection* method params id))
  ([method params]
    (json-rpc method params (uuid))) ; XXX: should I use uuid or nil as default id?
  ([host method params id]
    (let [connection (json-rpc-connect host)]
      (with-open [s (:socket connection)]
        (json-rpc-with connection method params id)))))

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;; json-rpc server
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

(def ^:dynamic *json-rpc-methods* nil)

(defn ^:dynamic json-rpc-lookup [method]
  (if-let [m (get *json-rpc-methods* method)]
    m
    (throw (Exception. (format "method %s is not found" method)))))

;; XXX: TODO: FIXME: how to close the socket? and how to keep it?
(defn json-rpc-service [ins outs]
  (let [in (reader ins)
        out (writer outs)]
    (loop []
      (when-let [{method "method" params "params" id "id"} (json-rpc-read in)]
        (try
          (json-rpc-write out {"result" (apply (json-rpc-lookup method) params) "error" nil "id" id})
          (catch Exception e
            (error e e)
            (json-rpc-write out {"result" nil "error" (.getMessage e) "id" id})))
        (recur)))))

(defn json-rpc-create-server [& {:keys [port methods]}]
  (let [port (or port *json-rpc-port*)]
    (binding [*json-rpc-methods* methods]
      (create-server port (bound-fn* json-rpc-service)))))

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;; test
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

;(json-rpc-create-server :methods {"inc" inc})
;(prn (json-rpc "localhost" "inc" [1] (uuid)))

